"use strict";

const chai = require( "chai" );
const {
    parser,
    util,
    ast,
} = require( "pegjs" );

const expect = chai.expect;

// better diagnostics for deep eq failure
chai.config.truncateThreshold = 0;

function varyParserOptions( block ) {

    const optionsVariants = [
        { },
        { extractComments: false },
        { extractComments: true },
    ];

    optionsVariants.forEach( variant => {

        describe(
            "with options " + chai.util.inspect( variant ),
            () => block( variant ),
        );

    } );

}

describe( "PEG.js grammar parser", function () {

    const literalAbcd       = { type: "literal", value: "abcd", ignoreCase: false };
    const literalEfgh       = { type: "literal", value: "efgh", ignoreCase: false };
    const literalIjkl       = { type: "literal", value: "ijkl", ignoreCase: false };
    const literalMnop       = { type: "literal", value: "mnop", ignoreCase: false };
    const semanticAnd       = { type: "semantic_and", code: " code " };
    const semanticNot       = { type: "semantic_not", code: " code " };
    const optional          = { type: "optional", expression: literalAbcd };
    const zeroOrMore        = { type: "zero_or_more", expression: literalAbcd };
    const oneOrMore         = { type: "one_or_more", expression: literalAbcd };
    const textOptional      = { type: "text", expression: optional };
    const simpleNotAbcd     = { type: "simple_not", expression: literalAbcd };
    const simpleAndOptional = { type: "simple_and", expression: optional };
    const simpleNotOptional = { type: "simple_not", expression: optional };
    const labeledAbcd       = { type: "labeled", label: "a", expression: literalAbcd };
    const labeledEfgh       = { type: "labeled", label: "b", expression: literalEfgh };
    const labeledIjkl       = { type: "labeled", label: "c", expression: literalIjkl };
    const labeledMnop       = { type: "labeled", label: "d", expression: literalMnop };
    const labeledSimpleNot  = { type: "labeled", label: "a", expression: simpleNotAbcd };
    const sequence          = {
        type: "sequence",
        elements: [ literalAbcd, literalEfgh, literalIjkl ],
    };
    const sequence2         = {
        type: "sequence",
        elements: [ labeledAbcd, labeledEfgh ],
    };
    const sequence4         = {
        type: "sequence",
        elements: [ labeledAbcd, labeledEfgh, labeledIjkl, labeledMnop ],
    };
    const groupLabeled      = { type: "group", expression: labeledAbcd };
    const groupSequence     = { type: "group", expression: sequence };
    const actionAbcd        = { type: "action", expression: literalAbcd, code: " code " };
    const actionEfgh        = { type: "action", expression: literalEfgh, code: " code " };
    const actionIjkl        = { type: "action", expression: literalIjkl, code: " code " };
    const actionMnop        = { type: "action", expression: literalMnop, code: " code " };
    const actionSequence    = { type: "action", expression: sequence, code: " code " };
    const choice            = {
        type: "choice",
        alternatives: [ literalAbcd, literalEfgh, literalIjkl ],
    };
    const choice2           = {
        type: "choice",
        alternatives: [ actionAbcd, actionEfgh ],
    };
    const choice4           = {
        type: "choice",
        alternatives: [ actionAbcd, actionEfgh, actionIjkl, actionMnop ],
    };
    const named             = { type: "named", name: "start rule", expression: literalAbcd };
    const ruleA             = { type: "rule", name: "a", expression: literalAbcd };
    const ruleB             = { type: "rule", name: "b", expression: literalEfgh };
    const ruleC             = { type: "rule", name: "c", expression: literalIjkl };
    const ruleStart         = { type: "rule", name: "start", expression: literalAbcd };
    const initializer       = { type: "initializer", code: " code " };

    function oneRuleGrammar( expression ) {

        return {
            type: "grammar",
            initializer: null,
            comments: null,
            rules: [ { type: "rule", name: "start", expression: expression } ],
        };

    }

    function actionGrammar( code ) {

        return oneRuleGrammar(
            { type: "action", expression: literalAbcd, code: code },
        );

    }

    function literalGrammar( value, ignoreCase ) {

        return oneRuleGrammar(
            { type: "literal", value: value, ignoreCase: ignoreCase },
        );

    }

    function classGrammar( parts, inverted, ignoreCase ) {

        return oneRuleGrammar( {
            type: "class",
            parts: parts,
            inverted: inverted,
            ignoreCase: ignoreCase,
        } );

    }

    function anyGrammar() {

        return oneRuleGrammar( { type: "any" } );

    }

    function ruleRefGrammar( name ) {

        return oneRuleGrammar( { type: "rule_ref", name: name } );

    }

    function commented( grammar, comments, options ) {

        function toObject( result, comment ) {

            result[ comment.offset ] = {
                text: comment.text,
                multiline: comment.multiline,
            };

            return result;

        }

        grammar = util.clone( grammar );
        grammar.comments = options.extractComments ? comments.reduce( toObject, {} ) : null;
        return grammar;

    }

    const trivialGrammar = literalGrammar( "abcd", false );
    const twoRuleGrammar = {
        type: "grammar",
        initializer: null,
        comments: null,
        rules: [ ruleA, ruleB ],
    };

    const stripProperties = ( function () {

        let strip;

        function stripLeaf( node ) {

            delete node.location;

        }

        function stripExpression( node ) {

            delete node.location;

            strip( node.expression );

        }

        function stripChildren( property ) {

            return function ( node ) {

                delete node.location;

                node[ property ].forEach( strip );

            };

        }

        strip = ast.visitor.build( {
            grammar( node ) {

                delete node.location;
                delete node._alwaysConsumesOnSuccess;

                if ( node.initializer ) {

                    strip( node.initializer );

                }
                if ( node.comments ) {

                    util.each( node.comments, stripLeaf );

                }
                node.rules.forEach( strip );

            },

            initializer: stripLeaf,
            rule: stripExpression,
            named: stripExpression,
            choice: stripChildren( "alternatives" ),
            action: stripExpression,
            sequence: stripChildren( "elements" ),
            labeled: stripExpression,
            text: stripExpression,
            simple_and: stripExpression,
            simple_not: stripExpression,
            optional: stripExpression,
            zero_or_more: stripExpression,
            one_or_more: stripExpression,
            group: stripExpression,
            semantic_and: stripLeaf,
            semantic_not: stripLeaf,
            rule_ref: stripLeaf,
            literal: stripLeaf,
            class: stripLeaf,
            any: stripLeaf,
        } );

        return strip;

    } )();

    function helpers( chai, utils ) {

        const Assertion = chai.Assertion;

        Assertion.addMethod( "parseAs", function ( expected, options ) {

            options = typeof options === "undefined" ? {} : options;

            const result = parser.parse( utils.flag( this, "object" ), options );

            stripProperties( result );

            this.assert(
                utils.eql( result, expected ),
                "expected #{this} to parse as #{exp} but got #{act}",
                "expected #{this} to not parse as #{exp}",
                expected,
                result,
                ! utils.flag( this, "negate" ),
            );

        } );

        Assertion.addMethod( "failToParse", function ( props ) {

            let passed, result;

            try {

                result = parser.parse( utils.flag( this, "object" ) );
                passed = true;

            } catch ( e ) {

                result = e;
                passed = false;

            }

            if ( passed ) {

                stripProperties( result );

            }

            this.assert(
                ! passed,
                "expected #{this} to fail to parse but got #{act}",
                "expected #{this} to not fail to parse but it failed with #{act}",
                null,
                result,
            );

            if ( ! passed && typeof props !== "undefined" ) {

                Object.keys( props ).forEach( key => {

                    new Assertion( result )
                        .to.have.property( key )
                        .that.is.deep.equal( props[ key ] );

                } );

            }

        } );

    }

    // Helper activation needs to put inside a |beforeEach| block because the
    // helpers conflict with the ones in
    // test/behavior/generated-parser-behavior.spec.js.
    beforeEach( function () {

        chai.use( helpers );

    } );

    // Grammars without any rules are not accepted.
    it( "parses Rule+", function () {

        expect( "start = a" ).to.parseAs( ruleRefGrammar( "a" ) );
        const grammar = ruleRefGrammar( "a" );
        grammar.initializer = {
            "type": "initializer",
            "code": "",
        };
        expect( "{}\nstart = a" ).to.parseAs( grammar );

        expect( "" ).to.failToParse();
        expect( "{}" ).to.failToParse();

    } );

    // Canonical Grammar is "a = 'abcd'; b = 'efgh'; c = 'ijkl';".
    it( "parses Grammar", function () {

        expect( "\na = 'abcd';\n" ).to.parseAs(
            { type: "grammar", comments: null, initializer: null, rules: [ ruleA ] },
        );
        expect( "\na = 'abcd';\nb = 'efgh';\nc = 'ijkl';\n" ).to.parseAs(
            { type: "grammar", comments: null, initializer: null, rules: [ ruleA, ruleB, ruleC ] },
        );
        expect( "\n{ code };\na = 'abcd';\n" ).to.parseAs(
            { type: "grammar", comments: null, initializer: initializer, rules: [ ruleA ] },
        );

    } );

    // Canonical Initializer is "{ code }".
    it( "parses Initializer", function () {

        expect( "{ code };start = 'abcd'" ).to.parseAs(
            { type: "grammar", comments: null, initializer: initializer, rules: [ ruleStart ] },
        );

    } );

    // Canonical Rule is "a = 'abcd';".
    it( "parses Rule", function () {

        expect( "start\n=\n'abcd';" ).to.parseAs(
            oneRuleGrammar( literalAbcd ),
        );
        expect( "start\n'start rule'\n=\n'abcd';" ).to.parseAs(
            oneRuleGrammar( named ),
        );

    } );

    // Canonical Expression is "'abcd'".
    it( "parses Expression", function () {

        expect( "start = 'abcd' / 'efgh' / 'ijkl'" ).to.parseAs(
            oneRuleGrammar( choice ),
        );

    } );

    // Canonical ChoiceExpression is "'abcd' / 'efgh' / 'ijkl'".
    it( "parses ChoiceExpression", function () {

        expect( "start = 'abcd' { code }" ).to.parseAs(
            oneRuleGrammar( actionAbcd ),
        );
        expect( "start = 'abcd' { code }\n/\n'efgh' { code }" ).to.parseAs(
            oneRuleGrammar( choice2 ),
        );
        expect(
            "start = 'abcd' { code }\n/\n'efgh' { code }\n/\n'ijkl' { code }\n/\n'mnop' { code }",
        ).to.parseAs(
            oneRuleGrammar( choice4 ),
        );

    } );

    // Canonical ActionExpression is "'abcd' { code }".
    it( "parses ActionExpression", function () {

        expect( "start = 'abcd' 'efgh' 'ijkl'" ).to.parseAs(
            oneRuleGrammar( sequence ),
        );
        expect( "start = 'abcd' 'efgh' 'ijkl'\n{ code }" ).to.parseAs(
            oneRuleGrammar( actionSequence ),
        );

    } );

    // Canonical SequenceExpression is "'abcd' 'efgh' 'ijkl'".
    it( "parses SequenceExpression", function () {

        expect( "start = a:'abcd'" ).to.parseAs(
            oneRuleGrammar( labeledAbcd ),
        );
        expect( "start = a:'abcd'\nb:'efgh'" ).to.parseAs(
            oneRuleGrammar( sequence2 ),
        );
        expect( "start = a:'abcd'\nb:'efgh'\nc:'ijkl'\nd:'mnop'" ).to.parseAs(
            oneRuleGrammar( sequence4 ),
        );

    } );

    // Value Plucking
    it( "parses `@` (value plucking)", function () {

        function $S( ...elements ) {

            return oneRuleGrammar( {
                type: "sequence",
                elements: elements,
            } );

        }
        function $P( label, expression ) {

            return {
                type: "labeled",
                pick: true,
                label: label,
                expression: expression,
            };

        }

        expect( "start = @'abcd'" ).to.parseAs(
            $S( $P( null, literalAbcd ) ),
        );
        expect( "start = @a:'abcd'" ).to.parseAs(
            $S( $P( "a", literalAbcd ) ),
        );
        expect( "start = 'abcd' @'efgh'" ).to.parseAs(
            $S( literalAbcd, $P( null, literalEfgh ) ),
        );
        expect( "start = a:'abcd' @b:'efgh'" ).to.parseAs(
            $S( labeledAbcd, $P( "b", literalEfgh ) ),
        );
        expect( "start = @'abcd' b:'efgh'" ).to.parseAs(
            $S( $P( null, literalAbcd ), labeledEfgh ),
        );
        expect( "start = a:'abcd' @'efgh' 'ijkl' @d:'mnop'" ).to.parseAs(
            $S( labeledAbcd, $P( null, literalEfgh ), literalIjkl, $P( "d", literalMnop ) ),
        );

    } );

    // Canonical LabeledExpression is "a:'abcd'".
    it( "parses LabeledExpression", function () {

        expect( "start = a\n:\n!'abcd'" ).to.parseAs( oneRuleGrammar( labeledSimpleNot ) );
        expect( "start = !'abcd'" ).to.parseAs( oneRuleGrammar( simpleNotAbcd ) );

    } );

    // Canonical PrefixedExpression is "!'abcd'".
    it( "parses PrefixedExpression", function () {

        expect( "start = !\n'abcd'?" ).to.parseAs( oneRuleGrammar( simpleNotOptional ) );
        expect( "start = 'abcd'?" ).to.parseAs( oneRuleGrammar( optional ) );

    } );

    // Canonical PrefixedOperator is "!".
    it( "parses PrefixedOperator", function () {

        expect( "start = $'abcd'?" ).to.parseAs( oneRuleGrammar( textOptional ) );
        expect( "start = &'abcd'?" ).to.parseAs( oneRuleGrammar( simpleAndOptional ) );
        expect( "start = !'abcd'?" ).to.parseAs( oneRuleGrammar( simpleNotOptional ) );

    } );

    // Canonical SuffixedExpression is "'abcd'?".
    it( "parses SuffixedExpression", function () {

        expect( "start = 'abcd'\n?" ).to.parseAs( oneRuleGrammar( optional ) );
        expect( "start = 'abcd'" ).to.parseAs( oneRuleGrammar( literalAbcd ) );

    } );

    // Canonical SuffixedOperator is "?".
    it( "parses SuffixedOperator", function () {

        expect( "start = 'abcd'?" ).to.parseAs( oneRuleGrammar( optional ) );
        expect( "start = 'abcd'*" ).to.parseAs( oneRuleGrammar( zeroOrMore ) );
        expect( "start = 'abcd'+" ).to.parseAs( oneRuleGrammar( oneOrMore ) );

    } );

    // Canonical PrimaryExpression is "'abcd'".
    it( "parses PrimaryExpression", function () {

        expect( "start = 'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start = [a-d]" ).to.parseAs( classGrammar( [ [ "a", "d" ] ], false, false ) );
        expect( "start = ." ).to.parseAs( anyGrammar() );
        expect( "start = a" ).to.parseAs( ruleRefGrammar( "a" ) );
        expect( "start = &{ code }" ).to.parseAs( oneRuleGrammar( semanticAnd ) );

        expect( "start = (\na:'abcd'\n)" ).to.parseAs( oneRuleGrammar( groupLabeled ) );
        expect( "start = (\n'abcd' 'efgh' 'ijkl'\n)" ).to.parseAs( oneRuleGrammar( groupSequence ) );
        expect( "start = (\n'abcd'\n)" ).to.parseAs( trivialGrammar );

    } );

    // Canonical RuleReferenceExpression is "a".
    it( "parses RuleReferenceExpression", function () {

        expect( "start = a" ).to.parseAs( ruleRefGrammar( "a" ) );

        expect( "start = a\n=" ).to.failToParse();
        expect( "start = a\n'abcd'\n=" ).to.failToParse();

    } );

    // Canonical SemanticPredicateExpression is "!{ code }".
    it( "parses SemanticPredicateExpression", function () {

        expect( "start = !\n{ code }" ).to.parseAs( oneRuleGrammar( semanticNot ) );

    } );

    // Canonical SemanticPredicateOperator is "!".
    it( "parses SemanticPredicateOperator", function () {

        expect( "start = &{ code }" ).to.parseAs( oneRuleGrammar( semanticAnd ) );
        expect( "start = !{ code }" ).to.parseAs( oneRuleGrammar( semanticNot ) );

    } );

    // The SourceCharacter rule is not tested.

    // Canonical WhiteSpace is " ".
    it( "parses WhiteSpace", function () {

        expect( "start =\t'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\v'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\f'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start = 'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\u00A0'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\uFEFF'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\u1680'abcd'" ).to.parseAs( trivialGrammar );

    } );

    // Canonical LineTerminator is "\n".
    it( "parses LineTerminator", function () {

        expect( "start = '\n'" ).to.failToParse();
        expect( "start = '\r'" ).to.failToParse();
        expect( "start = '\u2028'" ).to.failToParse();
        expect( "start = '\u2029'" ).to.failToParse();

    } );

    // Canonical LineTerminatorSequence is "\r\n".
    it( "parses LineTerminatorSequence", function () {

        expect( "start =\n'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\r\n'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\r'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\u2028'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\u2029'abcd'" ).to.parseAs( trivialGrammar );

    } );

    varyParserOptions( function ( options ) {

        // Canonical Comment is "/* comment */".
        it( "parses Comment", function () {

            expect( "start =// comment\n'abcd'" ).to.parseAs( commented( trivialGrammar, [ { offset: 7, text: " comment", multiline: false } ], options ), options );
            expect( "start =/* comment */'abcd'" ).to.parseAs( commented( trivialGrammar, [ { offset: 7, text: " comment ", multiline: true } ], options ), options );

        } );

        // Canonical MultiLineComment is "/* comment */".
        it( "parses MultiLineComment", function () {

            expect( "start =/**/'abcd'" ).to.parseAs( commented( trivialGrammar, [ { offset: 7, text: "", multiline: true } ], options ), options );
            expect( "start =/*a*/'abcd'" ).to.parseAs( commented( trivialGrammar, [ { offset: 7, text: "a", multiline: true } ], options ), options );
            expect( "start =/*abc*/'abcd'" ).to.parseAs( commented( trivialGrammar, [ { offset: 7, text: "abc", multiline: true } ], options ), options );

            expect( "start =/**/*/'abcd'" ).to.failToParse();

        } );

        // Canonical MultiLineCommentNoLineTerminator is "/* comment */".
        it( "parses MultiLineCommentNoLineTerminator", function () {

            expect( "a = 'abcd'/**/\r\nb = 'efgh'" ).to.parseAs( commented( twoRuleGrammar, [ { offset: 10, text: "", multiline: true } ], options ), options );
            expect( "a = 'abcd'/*a*/\r\nb = 'efgh'" ).to.parseAs( commented( twoRuleGrammar, [ { offset: 10, text: "a", multiline: true } ], options ), options );
            expect( "a = 'abcd'/*abc*/\r\nb = 'efgh'" ).to.parseAs( commented( twoRuleGrammar, [ { offset: 10, text: "abc", multiline: true } ], options ), options );

            expect( "a = 'abcd'/**/*/\r\nb = 'efgh'" ).to.failToParse();
            expect( "a = 'abcd'/*\n*/\r\nb = 'efgh'" ).to.failToParse();

        } );

        // Canonical SingleLineComment is "// comment".
        it( "parses SingleLineComment", function () {

            expect( "start =//\n'abcd'" ).to.parseAs( commented( trivialGrammar, [ { offset: 7, text: "", multiline: false } ], options ), options );
            expect( "start =//a\n'abcd'" ).to.parseAs( commented( trivialGrammar, [ { offset: 7, text: "a", multiline: false } ], options ), options );
            expect( "start =//abc\n'abcd'" ).to.parseAs( commented( trivialGrammar, [ { offset: 7, text: "abc", multiline: false } ], options ), options );

            expect( "start =//\n>\n'abcd'" ).to.failToParse();

        } );

    } );

    // Canonical Identifier is "a".
    it( "parses Identifier", function () {

        expect( "start = a:'abcd'" ).to.parseAs( oneRuleGrammar( labeledAbcd ) );

    } );

    // Canonical IdentifierName is "a".
    it( "parses IdentifierName", function () {

        expect( "start = a" ).to.parseAs( ruleRefGrammar( "a" ) );
        expect( "start = ab" ).to.parseAs( ruleRefGrammar( "ab" ) );
        expect( "start = abcd" ).to.parseAs( ruleRefGrammar( "abcd" ) );

    } );

    // Canonical IdentifierStart is "a".
    it( "parses IdentifierStart", function () {

        expect( "start = a" ).to.parseAs( ruleRefGrammar( "a" ) );
        expect( "start = $" ).to.parseAs( ruleRefGrammar( "$" ) );
        expect( "start = _" ).to.parseAs( ruleRefGrammar( "_" ) );
        expect( "start = \\u0061" ).to.parseAs( ruleRefGrammar( "a" ) );

    } );

    // Canonical IdentifierPart is "a".
    it( "parses IdentifierPart", function () {

        expect( "start = aa" ).to.parseAs( ruleRefGrammar( "aa" ) );
        expect( "start = a\u0300" ).to.parseAs( ruleRefGrammar( "a\u0300" ) );
        expect( "start = a0" ).to.parseAs( ruleRefGrammar( "a0" ) );
        expect( "start = a\u203F" ).to.parseAs( ruleRefGrammar( "a\u203F" ) );
        expect( "start = a\u200C" ).to.parseAs( ruleRefGrammar( "a\u200C" ) );
        expect( "start = a\u200D" ).to.parseAs( ruleRefGrammar( "a\u200D" ) );

    } );

    // Unicode rules and reserved word rules are not tested.

    // Canonical LiteralMatcher is "'abcd'".
    it( "parses LiteralMatcher", function () {

        expect( "start = 'abcd'" ).to.parseAs( literalGrammar( "abcd", false ) );
        expect( "start = 'abcd'i" ).to.parseAs( literalGrammar( "abcd", true ) );

    } );

    // Canonical StringLiteral is "'abcd'".
    it( "parses StringLiteral", function () {

        expect( "start = \"\"" ).to.parseAs( literalGrammar( "", false ) );
        expect( "start = \"a\"" ).to.parseAs( literalGrammar( "a", false ) );
        expect( "start = \"abc\"" ).to.parseAs( literalGrammar( "abc", false ) );

        expect( "start = ''" ).to.parseAs( literalGrammar( "", false ) );
        expect( "start = 'a'" ).to.parseAs( literalGrammar( "a", false ) );
        expect( "start = 'abc'" ).to.parseAs( literalGrammar( "abc", false ) );

    } );

    // Canonical DoubleStringCharacter is "a".
    it( "parses DoubleStringCharacter", function () {

        expect( "start = \"a\"" ).to.parseAs( literalGrammar( "a", false ) );
        expect( "start = \"\\n\"" ).to.parseAs( literalGrammar( "\n", false ) );
        expect( "start = \"\\\n\"" ).to.parseAs( literalGrammar( "", false ) );

        expect( "start = \"\"\"" ).to.failToParse();
        expect( "start = \"\\\"" ).to.failToParse();
        expect( "start = \"\n\"" ).to.failToParse();

    } );

    // Canonical SingleStringCharacter is "a".
    it( "parses SingleStringCharacter", function () {

        expect( "start = 'a'" ).to.parseAs( literalGrammar( "a", false ) );
        expect( "start = '\\n'" ).to.parseAs( literalGrammar( "\n", false ) );
        expect( "start = '\\\n'" ).to.parseAs( literalGrammar( "", false ) );

        expect( "start = '''" ).to.failToParse();
        expect( "start = '\\'" ).to.failToParse();
        expect( "start = '\n'" ).to.failToParse();

    } );

    // Canonical CharacterClassMatcher is "[a-d]".
    it( "parses CharacterClassMatcher", function () {

        expect( "start = []" ).to.parseAs(
            classGrammar( [], false, false ),
        );
        expect( "start = [a-d]" ).to.parseAs(
            classGrammar( [ [ "a", "d" ] ], false, false ),
        );
        expect( "start = [a]" ).to.parseAs(
            classGrammar( [ "a" ], false, false ),
        );
        expect( "start = [a-de-hi-l]" ).to.parseAs(
            classGrammar(
                [ [ "a", "d" ], [ "e", "h" ], [ "i", "l" ] ],
                false,
                false,
            ),
        );
        expect( "start = [^a-d]" ).to.parseAs(
            classGrammar( [ [ "a", "d" ] ], true, false ),
        );
        expect( "start = [a-d]i" ).to.parseAs(
            classGrammar( [ [ "a", "d" ] ], false, true ),
        );

        expect( "start = [\\\n]" ).to.parseAs(
            classGrammar( [], false, false ),
        );

    } );

    // Canonical ClassCharacterRange is "a-d".
    it( "parses ClassCharacterRange", function () {

        expect( "start = [a-d]" ).to.parseAs( classGrammar( [ [ "a", "d" ] ], false, false ) );

        expect( "start = [a-a]" ).to.parseAs( classGrammar( [ [ "a", "a" ] ], false, false ) );
        expect( "start = [b-a]" ).to.failToParse( {
            message: "Invalid character range: b-a.",
        } );

    } );

    // Canonical ClassCharacter is "a".
    it( "parses ClassCharacter", function () {

        expect( "start = [a]" ).to.parseAs( classGrammar( [ "a" ], false, false ) );
        expect( "start = [\\n]" ).to.parseAs( classGrammar( [ "\n" ], false, false ) );
        expect( "start = [\\\n]" ).to.parseAs( classGrammar( [], false, false ) );

        expect( "start = []]" ).to.failToParse();
        expect( "start = [\\]" ).to.failToParse();
        expect( "start = [\n]" ).to.failToParse();

    } );

    // Canonical LineContinuation is "\\\n".
    it( "parses LineContinuation", function () {

        expect( "start = '\\\r\n'" ).to.parseAs( literalGrammar( "", false ) );

    } );

    // Canonical EscapeSequence is "n".
    it( "parses EscapeSequence", function () {

        expect( "start = '\\n'" ).to.parseAs( literalGrammar( "\n", false ) );
        expect( "start = '\\0'" ).to.parseAs( literalGrammar( "\x00", false ) );
        expect( "start = '\\xFF'" ).to.parseAs( literalGrammar( "\xFF", false ) );
        expect( "start = '\\uFFFF'" ).to.parseAs( literalGrammar( "\uFFFF", false ) );

        expect( "start = '\\09'" ).to.failToParse();

    } );

    // Canonical CharacterEscapeSequence is "n".
    it( "parses CharacterEscapeSequence", function () {

        expect( "start = '\\n'" ).to.parseAs( literalGrammar( "\n", false ) );
        expect( "start = '\\a'" ).to.parseAs( literalGrammar( "a", false ) );

    } );

    // Canonical SingleEscapeCharacter is "n".
    it( "parses SingleEscapeCharacter", function () {

        expect( "start = '\\''" ).to.parseAs( literalGrammar( "'", false ) );
        expect( "start = '\\\"'" ).to.parseAs( literalGrammar( "\"", false ) );
        expect( "start = '\\\\'" ).to.parseAs( literalGrammar( "\\", false ) );
        expect( "start = '\\b'" ).to.parseAs( literalGrammar( "\b", false ) );
        expect( "start = '\\f'" ).to.parseAs( literalGrammar( "\f", false ) );
        expect( "start = '\\n'" ).to.parseAs( literalGrammar( "\n", false ) );
        expect( "start = '\\r'" ).to.parseAs( literalGrammar( "\r", false ) );
        expect( "start = '\\t'" ).to.parseAs( literalGrammar( "\t", false ) );
        expect( "start = '\\v'" ).to.parseAs( literalGrammar( "\v", false ) );

    } );

    // Canonical NonEscapeCharacter is "a".
    it( "parses NonEscapeCharacter", function () {

        expect( "start = '\\a'" ).to.parseAs( literalGrammar( "a", false ) );

    } );

    // The negative predicate is impossible to test with PEG.js grammar structure.

    // The EscapeCharacter rule is impossible to test with PEG.js grammar structure.

    // Canonical HexEscapeSequence is "xFF".
    it( "parses HexEscapeSequence", function () {

        expect( "start = '\\xFF'" ).to.parseAs( literalGrammar( "\xFF", false ) );

    } );

    // Canonical UnicodeEscapeSequence is "uFFFF".
    it( "parses UnicodeEscapeSequence", function () {

        expect( "start = '\\uFFFF'" ).to.parseAs( literalGrammar( "\uFFFF", false ) );

    } );

    // Digit rules are not tested.

    // Canonical AnyMatcher is ".".
    it( "parses AnyMatcher", function () {

        expect( "start = ." ).to.parseAs( anyGrammar() );

    } );

    // Canonical CodeBlock is "{ code }".
    it( "parses CodeBlock", function () {

        expect( "start = 'abcd' { code }" ).to.parseAs( actionGrammar( " code " ) );

    } );

    // Canonical Code is " code ".
    it( "parses Code", function () {

        expect( "start = 'abcd' {a}" ).to.parseAs( actionGrammar( "a" ) );
        expect( "start = 'abcd' {abc}" ).to.parseAs( actionGrammar( "abc" ) );
        expect( "start = 'abcd' {{a}}" ).to.parseAs( actionGrammar( "{a}" ) );
        expect( "start = 'abcd' {{a}{b}{c}}" ).to.parseAs( actionGrammar( "{a}{b}{c}" ) );

        expect( "start = 'abcd' {{}" ).to.failToParse();
        expect( "start = 'abcd' {}}" ).to.failToParse();

    } );

    // Unicode character category rules and token rules are not tested.

    // Canonical __ is "\n".
    it( "parses __", function () {

        expect( "start ='abcd'" ).to.parseAs( trivialGrammar );
        expect( "start = 'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =\r\n'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =/* comment */'abcd'" ).to.parseAs( trivialGrammar );
        expect( "start =   'abcd'" ).to.parseAs( trivialGrammar );

    } );

    // Canonical _ is " ".
    it( "parses _", function () {

        expect( "a = 'abcd'\r\nb = 'efgh'" ).to.parseAs( twoRuleGrammar );
        expect( "a = 'abcd' \r\nb = 'efgh'" ).to.parseAs( twoRuleGrammar );
        expect( "a = 'abcd'/* comment */\r\nb = 'efgh'" ).to.parseAs( twoRuleGrammar );
        expect( "a = 'abcd'   \r\nb = 'efgh'" ).to.parseAs( twoRuleGrammar );

    } );

    // Canonical EOS is ";".
    it( "parses EOS", function () {

        expect( "a = 'abcd'\n;b = 'efgh'" ).to.parseAs( twoRuleGrammar );
        expect( "a = 'abcd' \r\nb = 'efgh'" ).to.parseAs( twoRuleGrammar );
        expect( "a = 'abcd' // comment\r\nb = 'efgh'" ).to.parseAs( twoRuleGrammar );
        expect( "a = 'abcd'\nb = 'efgh'" ).to.parseAs( twoRuleGrammar );

    } );

    // Canonical EOF is the end of input.
    it( "parses EOF", function () {

        expect( "start = 'abcd'\n" ).to.parseAs( trivialGrammar );

    } );

    it( "reports unmatched brace", function () {

        const text = "rule = \n 'x' { y \n z";
        const errorLocation = {
            start: { offset: 13, line: 2, column: 6 },
            end: { offset: 14, line: 2, column: 7 },
        };
        expect( () => parser.parse( text ) )
            .to.throw( "Unbalanced brace." )
            .with.property( "location" )
            .that.deep.equals( errorLocation );

    } );

} );
